//@version=6
// Fibonacci Master
//
// The Fibonacci retracement, drawn for you and kept honest. The script finds the swing
// that matters, draws the levels from it, lights up the 0.5 to 0.618 zone where
// pullbacks are supposed to end, and projects the 1.272 and 1.618 targets beyond the
// swing. Then it measures instead of assuming. Every pullback on this chart is recorded
// as a fraction of its swing, so the chart carries its own answer to where pullbacks
// actually end here, drawn as a band beside the classic zone, along with how many of
// them went all the way and broke the swing instead of ending. Every pullback
// into the zone is a test, judged on closes: back past 0.382 and it HELD, past 0.786
// and it FAILED. Tests are tagged, boxed and counted. A hold locks the swing so the
// targets stay put, and whether the target was reached is counted too.
//
// The levels do the drawing, the zone does the reading, the band does the measuring,
// the tags keep the record, the panel does the words, the alerts carry the events.

indicator("Fibonacci Master [SimpleAlgo]", "Fibonacci Master [SimpleAlgo]", overlay = true, max_lines_count = 100, max_labels_count = 200, max_boxes_count = 100)

// ───────────────────────── Settings ─────────────────────────

grpS = "Swing"
minSwing = input.float(4, "Swing size (ATR)", minval = 1, maxval = 20, step = 0.5, group = grpS, tooltip = "The smallest move that counts as a swing, in ATRs. Levels appear once the swing is this tall, and a pullback this deep followed by a new high starts a fresh swing from the pullback low. Raise it for bigger swings, lower it for smaller ones.")

grpL = "Look"
themeIn = input.string("Neon", "Theme", options = ["Neon", "Classic", "Aurora", "Ice", "Custom"], group = grpL)
upColIn = input.color(#00FFA3, "Up swing", group = grpL, inline = "c")
dnColIn = input.color(#FF3D71, "Down swing", group = grpL, inline = "c")
bright  = input.int(3, "Brightness", minval = 1, maxval = 5, group = grpL)
showExt  = input.bool(true, "Targets (1.272, 1.618)", group = grpL, inline = "a")
showBand = input.bool(true, "Where pullbacks end here", group = grpL, inline = "a", tooltip = "A dotted band between the 25th and 75th percentile of where the last twenty pullbacks on this chart ended, measured as a fraction of their swing. Pullbacks that broke the swing are counted but kept out of the band.")
showSwing = input.bool(true, "Swing line", group = grpL, inline = "b")
showPast  = input.bool(true, "Past zones", group = grpL, inline = "b", tooltip = "A faint box where each judged zone sat, coloured by what happened.")
showTags = input.bool(true, "HELD / FAILED tags", group = grpL, inline = "d")
showLbl  = input.bool(true, "Level labels", group = grpL, inline = "d")
aheadIn  = input.int(25, "Extend right (bars)", minval = 5, maxval = 100, group = grpL)
showPanel = input.bool(true, "Panel", group = grpL, inline = "p")
panelPos  = input.string("Top right", "", options = ["Top left", "Top right", "Bottom left", "Bottom right"], group = grpL, inline = "p")
panelSize = input.string("Normal", "", options = ["Small", "Normal", "Large"], group = grpL, inline = "p")

[colUp, colDn, colPanel] = switch themeIn
    "Neon"    => [#00FFA3, #FF3D71, #262B38]
    "Classic" => [#00FF31, #FF0013, #23262F]
    "Aurora"  => [#34D399, #F472B6, #262B38]
    "Ice"     => [#7DD3FC, #FDA4AF, #1E293B]
    => [upColIn, dnColIn, #262B38]
color colExt  = #7B9CFF
color colLine = #8A93A8
color colEdge = #C9CEDA
color colFail = #5F6677

// fixed rules
int   keepRec = 10      // zone tests remembered
int   keepPB  = 20      // pullbacks remembered for the band
float minPB   = 0.236   // a dip shallower than this is not a pullback

// ───────────────────────── The swing ─────────────────────────
// One swing at a time. An up swing runs from its start low to the highest high since.
// A pullback as big as a swing itself counts as a swing low once price makes a new
// high past it, and the swing restarts from the most recent such low; smaller dips
// just extend the swing. A close past 0.786 means the pullback has become the swing,
// so the swing flips the other way and runs from the old high to the new low.
// Once a pullback to the zone has held, the swing is locked so the targets stay put:
// it unlocks when price reaches 1.618, loses the pullback low, or pulls back again
// and makes a new high, at which point the swing restarts from the low that held.
// Every pullback that ends is measured as a fraction of its swing and remembered.

float atr = math.max(nz(ta.atr(14), high - low), syminfo.mintick)
float regDepth = minSwing * atr     // a pullback this deep registers as a swing low

var int   dir = 1
var float sP  = na        // swing start
var int   sB  = na
var float eP  = na        // swing end, the running extreme
var int   eB  = na
var float pbP = na        // pullback extreme since the swing end
var int   pbB = na
var bool  locked = false
var float lkE  = na       // while locked: the running extreme past the swing end
var int   lkEb = na
var float lkPb = na       // while locked: the pullback extreme since that
var int   lkPbB = na
var float heldAt = na     // while locked: how deep the pullback that held went
var bool  hit1272 = false
var candP = array.new_float()   // registered pullback lows, oldest first
var candB = array.new_int()
var pbRatios = array.new_float()   // where the last pullbacks ended, as a fraction of their swing
var pbBroke  = array.new_int()     // 1 if that pullback broke the swing instead of ending
var tgtRec   = array.new_int()     // after a hold: 1 if 1.272 was reached, 0 if not

bool flipped  = false
bool newExt   = false
bool unlocked = false
bool hit1618  = false

pushRatio(float r, int broke) =>
    array.push(pbRatios, math.min(r, 1.0))
    array.push(pbBroke, broke)
    if array.size(pbRatios) > keepPB
        array.shift(pbRatios)
        array.shift(pbBroke)

pushTgt(int v) =>
    array.push(tgtRec, v)
    if array.size(tgtRec) > keepRec
        array.shift(tgtRec)

if na(sP)
    sP := low
    sB := bar_index
    eP := high
    eB := bar_index
    pbP := high
    pbB := bar_index

// the swing is handled in one direction: s flips the comparisons for a down swing
float s = dir == 1 ? 1.0 : -1.0
float H = dir == 1 ? high : low      // the extreme in the swing's direction
float L = dir == 1 ? low : high      // the extreme against it

float legNow = math.abs(eP - sP)
bool  pastEnd = s * (close - sP) < 0
bool  past786 = legNow >= minSwing * atr and s * (close - (eP - 0.786 * (eP - sP))) < 0
if pastEnd or past786
    // a close past 0.786 of an established swing, or past the start of a forming one:
    // the pullback is now the swing
    if legNow >= minSwing * atr
        pushRatio(s * (eP - (dir == 1 ? math.min(pbP, low) : math.max(pbP, high))) / legNow, 1)
    if locked
        pushTgt(hit1272 ? 1 : 0)
    dir := -dir
    sP := eP
    sB := eB
    eP := L
    eB := bar_index
    pbP := L
    pbB := bar_index
    locked := false
    flipped := true
    array.clear(candP)
    array.clear(candB)
else if locked
    if s * (H - (eP + 0.618 * (eP - sP))) >= 0
        // 1.618 reached: the swing restarts from the low that held
        pushRatio(heldAt, 0)
        pushTgt(1)
        hit1618 := true
        sP := pbP
        sB := pbB
        eP := H
        eB := bar_index
        pbP := H
        pbB := bar_index
        locked := false
        newExt := true
        array.clear(candP)
        array.clear(candB)
    else if s * (close - pbP) < 0
        // the low that held is lost: back to the ordinary swing
        pushTgt(hit1272 ? 1 : 0)
        locked := false
        unlocked := true
        pbP := L
        pbB := bar_index
    else if s * (H - lkE) > 0 and lkPbB > lkEb and s * (lkE - lkPb) >= regDepth
        // a real pullback past the swing end and a new high after it: restart from the
        // low that held, and let that newer pullback low take over if it makes a full swing
        pushRatio(heldAt, 0)
        float legHeld = s * (lkE - pbP)
        if legHeld > 0 and s * (lkE - lkPb) / legHeld >= minPB
            pushRatio(s * (lkE - lkPb) / legHeld, 0)
        pushTgt(hit1272 ? 1 : 0)
        sP := pbP
        sB := pbB
        eP := H
        eB := bar_index
        pbP := H
        pbB := bar_index
        locked := false
        newExt := true
        array.clear(candP)
        array.clear(candB)
        if s * (H - lkPb) >= minSwing * atr
            sP := lkPb
            sB := lkPbB
        else
            array.push(candP, lkPb)
            array.push(candB, lkPbB)
    else if s * (H - lkE) > 0
        lkE := H
        lkEb := bar_index
        lkPb := H
        lkPbB := bar_index
    else if s * (L - lkPb) < 0
        lkPb := L
        lkPbB := bar_index
else
    if s * (H - eP) > 0
        // a new extreme. The pullback that just ended is measured, registers as a swing
        // low if it was deep enough, and pullback lows it went under are no longer swing lows
        if pbB > eB and legNow > 0 and s * (eP - pbP) / legNow >= minPB
            pushRatio(s * (eP - pbP) / legNow, 0)
        if pbB > eB and s * (eP - pbP) >= regDepth
            if array.size(candP) > 0
                for k = array.size(candP) - 1 to 0
                    if s * (array.get(candP, k) - pbP) >= 0
                        array.remove(candP, k)
                        array.remove(candB, k)
            array.push(candP, pbP)
            array.push(candB, pbB)
        eP := H
        eB := bar_index
        pbP := H
        pbB := bar_index
        newExt := true
    else if s * (L - pbP) < 0
        pbP := L
        pbB := bar_index
    // the swing restarts from the most recent registered low that gives a full-size swing
    int found = -1
    if array.size(candP) > 0
        for k = array.size(candP) - 1 to 0
            if found < 0 and s * (eP - array.get(candP, k)) >= minSwing * atr
                found := k
    if found >= 0
        sP := array.get(candP, found)
        sB := array.get(candB, found)
        for j = 0 to found
            array.shift(candP)
            array.shift(candB)

bool  up   = dir == 1
float rng  = eP - sP                       // signed: positive for an up swing
float leg  = math.abs(rng)
bool  showing = leg >= minSwing * atr

// a level's price, retracement ratios between 0 and 1, extensions above 1
lvl(float r) =>
    eP - r * rng

float z236 = lvl(0.236)
float z382 = lvl(0.382)
float z500 = lvl(0.5)
float z618 = lvl(0.618)
float z786 = lvl(0.786)
float t1272 = eP + 0.272 * rng
float t1618 = eP + 0.618 * rng

// how far price has pulled back from the swing end, as a ratio of the swing
float rDeep = leg > 0 ? (up ? (eP - pbP) / leg : (pbP - eP) / leg) : 0

// where pullbacks end on this chart: the middle half of the last twenty that ended.
// The ones that broke the swing are counted separately
int   nPB  = array.size(pbRatios)
int   nBroke = nPB > 0 ? int(array.sum(pbBroke)) : 0
var ended = array.new_float()
array.clear(ended)
if nPB > 0
    for k = 0 to nPB - 1
        if array.get(pbBroke, k) == 0
            array.push(ended, array.get(pbRatios, k))
bool  haveBand = array.size(ended) >= 5
float p25 = na
float p75 = na
if haveBand
    p25 := array.percentile_linear_interpolation(ended, 25)
    p75 := array.percentile_linear_interpolation(ended, 75)

// ───────────────────────── Zone tests and the record ─────────────────────────
// A pullback into the 0.5 to 0.618 zone is a test. It holds when a close gets back
// past 0.382, it fails when a close goes past 0.786. One test per pullback, and a
// held low that is lost re-arms the zone, because that is a retest.

var bool  armed   = true
var bool  inTest  = false
var int   testBar = na
var float testExt = na
var int   testExtBar = na
var bool  testUp  = true
var int   testEB  = na
var float testZ1  = na
var float testZ2  = na
var int   heldBar = -1
var int   failBar = -1
var int   flipBar = -1
var rec   = array.new_int()
var tags  = array.new_label()
var zones = array.new_box()

bool held   = false
bool failed = false
bool enterZone = false
bool hitNow = false

if newExt or flipped
    if inTest
        if newExt
            held := true
        else
            failed := true
    armed := true
    hit1272 := false
if flipped
    flipBar := bar_index
    heldBar := -1
    failBar := -1
if unlocked
    armed := true

if showing and not newExt and not flipped
    if inTest
        if up ? close >= z382 : close <= z382
            held := true
        else if up ? close < z786 : close > z786
            failed := true
    else if armed and bar_index > eB and (up ? low <= z500 : high >= z500)
        inTest := true
        enterZone := true
        testBar := bar_index
        testExt := up ? low : high
        testExtBar := bar_index
        testUp := up
        testEB := eB
        testZ1 := z500
        testZ2 := z618
        if up ? close >= z382 : close <= z382
            held := true
    if inTest and not held and not failed
        if up ? low < testExt : high > testExt
            testExt := up ? low : high
            testExtBar := bar_index

placeTag(string txt, color bg) =>
    label tg = label.new(testExtBar, testExt, txt, style = testUp ? label.style_label_up : label.style_label_down, color = bg, textcolor = #0f0b16, size = size.small)
    array.push(tags, tg)
    if array.size(tags) > 40
        label.delete(array.shift(tags))

placeZone(color c) =>
    box bx = box.new(testEB, math.max(testZ1, testZ2), bar_index, math.min(testZ1, testZ2), border_color = color.new(#000000, 100), bgcolor = color.new(c, 88))
    array.push(zones, bx)
    if array.size(zones) > 40
        box.delete(array.shift(zones))

if held or failed
    if held
        array.push(rec, 1)
        heldBar := bar_index
        if not newExt
            heldAt := math.abs(eP - testExt) / leg
            locked := true
            lkE := eP
            lkEb := eB
            lkPb := eP
            lkPbB := eB
    else
        array.push(rec, 0)
        failBar := bar_index
    if array.size(rec) > keepRec
        array.shift(rec)
    color outcome = held ? (testUp ? colUp : colDn) : colFail
    if showTags
        placeTag(held ? "HELD" : "FAILED", outcome)
    if showPast
        placeZone(outcome)
    inTest := false
    armed := false

// targets while the swing is locked
if locked and not hit1272 and s * (H - t1272) >= 0
    hit1272 := true
    hitNow := true

// ───────────────────────── Drawing ─────────────────────────

float brightMul = 0.4 + 0.2 * bright
tp(float alpha) =>
    int(math.round(100 - math.min(math.max(alpha * brightMul, 0.0), 1.0) * 100))

color colSw = up ? colUp : colDn

var line lSwing = line.new(na, na, na, na, style = line.style_dashed, width = 1)
var line l0   = line.new(na, na, na, na, width = 1)
var line l1   = line.new(na, na, na, na, width = 1)
var line l236 = line.new(na, na, na, na, width = 1)
var line l786 = line.new(na, na, na, na, width = 1)
var line l382 = line.new(na, na, na, na, width = 1)
var line l500 = line.new(na, na, na, na, width = 1)
var line l618 = line.new(na, na, na, na, width = 2)
var line h382 = line.new(na, na, na, na, width = 6)
var line h500 = line.new(na, na, na, na, width = 6)
var line h618 = line.new(na, na, na, na, width = 8)
var line zA   = line.new(na, na, na, na, color = na)
var line zB   = line.new(na, na, na, na, color = na)
var line x1272 = line.new(na, na, na, na, style = line.style_dashed, width = 1)
var line x1618 = line.new(na, na, na, na, style = line.style_dashed, width = 1)
var line xA   = line.new(na, na, na, na, color = na)
var line xB   = line.new(na, na, na, na, color = na)
var line bLo  = line.new(na, na, na, na, style = line.style_dotted, width = 1)
var line bHi  = line.new(na, na, na, na, style = line.style_dotted, width = 1)
var linefill f1 = linefill.new(l382, l500, color.new(#000000, 100))
var linefill f2 = linefill.new(l500, zA, color.new(#000000, 100))
var linefill f3 = linefill.new(zA, zB, color.new(#000000, 100))
var linefill f4 = linefill.new(zB, l618, color.new(#000000, 100))
var linefill fx1 = linefill.new(x1272, xA, color.new(#000000, 100))
var linefill fx2 = linefill.new(xA, xB, color.new(#000000, 100))
var linefill fx3 = linefill.new(xB, x1618, color.new(#000000, 100))
var linefill fB  = linefill.new(bLo, bHi, color.new(#000000, 100))
var label dS = label.new(na, na, "", style = label.style_circle, size = size.tiny)
var label dE = label.new(na, na, "", style = label.style_circle, size = size.tiny)
var label gS = label.new(na, na, "", style = label.style_circle, size = size.small)
var label gE = label.new(na, na, "", style = label.style_circle, size = size.small)
var label bL = label.new(na, na, "", style = label.style_label_lower_right, color = color.new(chart.bg_color, 15), textcolor = colExt, size = size.small)
var lblLv = array.new_label()
if barstate.isfirst
    for i = 0 to 8
        array.push(lblLv, label.new(na, na, "", style = label.style_label_left, color = color.new(chart.bg_color, 15), textcolor = colLine, size = size.normal))

put(line ln, int x1, int x2, float y, color c) =>
    line.set_xy1(ln, x1, y)
    line.set_xy2(ln, x2, y)
    line.set_color(ln, c)

tag(int i, int x, float y, string t, color c) =>
    label lb = array.get(lblLv, i)
    label.set_xy(lb, x, y)
    label.set_text(lb, showLbl ? t : "")
    label.set_textcolor(lb, c)

if barstate.islast
    int xR = bar_index + aheadIn
    // drawings cannot reach more than 10,000 bars back, so a very old swing start is clamped
    int xS = math.max(sB, bar_index - 9500)
    int xE = math.max(eB, bar_index - 9500)
    bool vis = showing
    bool swingVis = vis and showSwing and xS == sB
    bool bandVis = vis and showBand and haveBand
    color cHide = color.new(#000000, 100)
    color cEdge = vis ? color.new(colEdge, 30) : cHide
    color cLine = vis ? color.new(colLine, 45) : cHide
    color cZone = vis ? colSw : cHide
    color cHalo = vis ? color.new(colSw, tp(0.14)) : cHide
    color cExt  = vis and showExt ? color.new(colExt, 40) : cHide
    color cBand = bandVis ? color.new(colExt, 25) : cHide
    line.set_xy1(lSwing, xS, sP)
    line.set_xy2(lSwing, xE, eP)
    line.set_color(lSwing, swingVis ? color.new(colLine, 35) : cHide)
    put(l0, xS, xR, sP, cEdge)
    put(l1, xE, xR, eP, cEdge)
    put(l236, xE, xR, z236, cLine)
    put(l786, xE, xR, z786, cLine)
    put(l382, xE, xR, z382, cZone)
    put(l500, xE, xR, z500, cZone)
    put(l618, xE, xR, z618, cZone)
    put(h382, xE, xR, z382, cHalo)
    put(h500, xE, xR, z500, cHalo)
    put(h618, xE, xR, z618, cHalo)
    put(zA, xE, xR, lvl(0.54), cHide)
    put(zB, xE, xR, lvl(0.58), cHide)
    put(x1272, xE, xR, t1272, cExt)
    put(x1618, xE, xR, t1618, cExt)
    put(xA, xE, xR, eP + 0.387 * rng, cHide)
    put(xB, xE, xR, eP + 0.503 * rng, cHide)
    linefill.set_color(f1, vis ? color.new(colSw, tp(0.06)) : cHide)
    linefill.set_color(f2, vis ? color.new(colSw, tp(0.12)) : cHide)
    linefill.set_color(f3, vis ? color.new(colSw, tp(0.18)) : cHide)
    linefill.set_color(f4, vis ? color.new(colSw, tp(0.26)) : cHide)
    linefill.set_color(fx1, vis and showExt ? color.new(colExt, tp(0.12)) : cHide)
    linefill.set_color(fx2, vis and showExt ? color.new(colExt, tp(0.07)) : cHide)
    linefill.set_color(fx3, vis and showExt ? color.new(colExt, tp(0.03)) : cHide)
    // where pullbacks end on this chart, drawn on this swing. Its label hangs off the
    // band's right end on the chart side, so it never meets the level labels, and off
    // whichever edge is further from price, so it never sits on the latest candles
    float yLo = bandVis ? lvl(p25) : eP
    float yHi = bandVis ? lvl(p75) : eP
    put(bLo, xE, xR, yLo, cBand)
    put(bHi, xE, xR, yHi, cBand)
    linefill.set_color(fB, bandVis ? color.new(colExt, tp(0.06)) : cHide)
    float yTop = math.max(yLo, yHi)
    float yBot = math.min(yLo, yHi)
    bool  topSide = math.abs(close - yTop) >= math.abs(close - yBot)
    label.set_xy(bL, xR, topSide ? yTop : yBot)
    label.set_style(bL, topSide ? label.style_label_lower_right : label.style_label_upper_right)
    label.set_text(bL, bandVis and showLbl ? "pullbacks end " + str.tostring(p25, "0.00") + " to " + str.tostring(p75, "0.00") + " here" : "")
    label.set_textcolor(bL, bandVis ? colExt : cHide)
    label.set_color(bL, bandVis and showLbl ? color.new(chart.bg_color, 15) : cHide)
    // the swing's two ends
    label.set_xy(dS, xS, sP)
    label.set_xy(dE, xE, eP)
    label.set_xy(gS, xS, sP)
    label.set_xy(gE, xE, eP)
    label.set_color(dS, swingVis ? color.new(colEdge, 10) : cHide)
    label.set_color(dE, swingVis ? color.new(colEdge, 10) : cHide)
    label.set_color(gS, swingVis ? color.new(colSw, tp(0.2)) : cHide)
    label.set_color(gE, swingVis ? color.new(colSw, tp(0.2)) : cHide)
    // the level labels
    color tEdge = vis ? colEdge : cHide
    color tLine = vis ? color.new(colLine, 20) : cHide
    color tZone = vis ? colSw : cHide
    color tExt  = vis and showExt ? colExt : cHide
    tag(0, xR, sP, "0", tEdge)
    tag(1, xR, eP, "1", tEdge)
    tag(2, xR, z236, "0.236", tLine)
    tag(3, xR, z382, "0.382", tZone)
    tag(4, xR, z500, "0.5", tZone)
    tag(5, xR, z618, "0.618", tZone)
    tag(6, xR, z786, "0.786", tLine)
    tag(7, xR, t1272, "1.272", tExt)
    tag(8, xR, t1618, "1.618", tExt)

// ───────────────────────── Panel ─────────────────────────

bars(int n) =>
    str.tostring(n) + (n == 1 ? " bar" : " bars")

px(float x) =>
    syminfo.mintick >= 1 ? str.tostring(x, "#,##0") : syminfo.mintick >= 0.01 ? str.tostring(x, "#,##0.00") : str.tostring(x, format.mintick)

r2(float x) =>
    str.tostring(math.max(x, 0), "0.00")

var table panel = na
if showPanel and barstate.islast
    if na(panel)
        tablePos = switch panelPos
            "Top left"     => position.top_left
            "Bottom left"  => position.bottom_left
            "Bottom right" => position.bottom_right
            => position.top_right
        panel := table.new(tablePos, 2, 7, bgcolor = color.new(colPanel, 15), frame_color = color.new(colPanel, 0), frame_width = 1, border_width = 0)
    txtSize = panelSize == "Small" ? size.small : panelSize == "Large" ? size.large : size.normal
    color txt = #D1D4DC
    color dim = color.new(#D1D4DC, 40)
    string pend = barstate.isconfirmed ? "" : ", confirms on close"
    string side = up ? "Up" : "Down"
    string swingTxt = showing ? side + " " + str.tostring(leg / sP * 100, "0.00") + "% over " + bars(eB - sB) : "Forming, " + str.tostring(leg / atr, "0.0") + " of " + str.tostring(minSwing, "0.#") + " ATR"
    // the pullback, and how it compares with the pullbacks this chart usually makes
    string forHere = haveBand and showing and not locked and rDeep > 0.02 ? (rDeep < p25 ? ", less than usual here" : rDeep > p75 ? ", more than usual here" : ", usual here") : ""
    string pbTxt = not showing ? "Waiting for the swing" : locked ? "Held, deepest " + r2(heldAt) + (up ? (close > eP ? ", now above the high" : "") : (close < eP ? ", now below the low" : "")) : rDeep <= 0.02 ? "None yet, at the " + (up ? "high" : "low") : r2(rDeep) + (rDeep < 0.382 ? ", shallow" : rDeep < 0.5 ? ", near the zone" : rDeep <= 0.618 ? ", in the zone" : rDeep <= 0.786 ? ", past the zone" : ", deep, swing at risk") + forHere
    color pbClr = not showing ? txt : locked ? colSw : rDeep >= 0.5 and rDeep <= 0.618 ? colSw : rDeep > 0.786 ? (up ? colDn : colUp) : txt
    // the zone record
    int   nRec = array.size(rec)
    float hRec = nRec > 0 ? array.sum(rec) : 0.0
    string recTxt = nRec == 0 ? "No tests judged yet" : "Held " + str.tostring(hRec, "0") + " of the last " + str.tostring(nRec) + (nRec == 1 ? " test" : " tests")
    string tend = nRec >= 5 ? (hRec / nRec >= 0.7 ? ", they tend to hold here" : hRec / nRec <= 0.3 ? ", they tend to fail here" : "") : ""
    // where pullbacks end here
    string bandTxt = haveBand ? "End at " + r2(p25) + " to " + r2(p75) + ", " + str.tostring(nBroke) + " of " + str.tostring(nPB) + " broke the swing" : "Not enough pullbacks yet"
    // the targets, and how often the first one was reached after a hold
    int   nT = array.size(tgtRec)
    float hT = nT > 0 ? array.sum(tgtRec) : 0.0
    string tgtTxt = not showing ? "" : hit1272 ? "1.272 hit, 1.618 at " + px(t1618) : "1.272 at " + px(t1272) + ", 1.618 at " + px(t1618)
    string tgtRecTxt = nT >= 3 ? ", hit after " + str.tostring(hT, "0") + " of the last " + str.tostring(nT) + " holds" : ""
    // the read
    string who = up ? "buyers" : "sellers"
    string read = flipped ? (failed ? "Zone failed, swing flipped, now " : "Swing flipped, now ") + str.lower(side) + pend : not showing ? "Waiting for a swing, " + str.tostring(leg / atr, "0.0") + " ATR so far" : locked ? ((up ? close < z382 : close > z382) ? "Zone held, back inside it, " + px(pbP) + " is the line" : hit1272 ? "Zone held, 1.272 hit, 1.618 at " + px(t1618) + (hitNow ? pend : "") : "Zone held, next target 1.272 at " + px(t1272) + tgtRecTxt) : inTest ? "In the zone, needs a close " + (up ? "above " : "below ") + px(z382) + " to hold" + (enterZone ? pend : "") : held ? "Zone held, " + who + " back in" + pend : failed ? "Zone failed, swing at risk" + pend : flipBar >= 0 and bar_index - flipBar <= 5 ? "Swing flipped " + bars(bar_index - flipBar) + " ago, now " + str.lower(side) : heldBar >= 0 and bar_index - heldBar <= 5 ? "Zone held " + bars(bar_index - heldBar) + " ago, " + who + " back in" : failBar >= 0 and bar_index - failBar <= 5 ? "Zone failed " + bars(bar_index - failBar) + " ago, swing at risk" : unlocked ? "Held low lost, zone retest" + pend : rDeep <= 0.02 ? "At the " + (up ? "high" : "low") + ", no pullback yet" : rDeep < 0.5 ? "Pulling back, " + r2(rDeep) + " so far, zone " + str.tostring(math.max(up ? close - z500 : z500 - close, 0) / atr, "0.0") + " ATR away" + tend : rDeep <= 0.618 ? "In the zone, already tested this pullback" : rDeep <= 0.786 ? "Past the zone, " + px(z786) + " is the last line" : "Deep pullback, swing start at " + px(sP)
    color readClr = flipped or (flipBar >= 0 and bar_index - flipBar <= 5 and not inTest and not held and not failed) ? colSw : not showing ? txt : locked or held or (heldBar >= 0 and bar_index - heldBar <= 5 and not failed) ? colSw : failed or (failBar >= 0 and bar_index - failBar <= 5) or rDeep > 0.786 ? (up ? colDn : colUp) : inTest or (rDeep >= 0.5 and rDeep <= 0.618) ? colSw : txt
    table.cell(panel, 0, 0, "FIBONACCI MASTER", text_color = txt, text_size = txtSize, text_halign = text.align_left)
    table.cell(panel, 1, 0, px(close), text_color = dim, text_size = txtSize, text_halign = text.align_right)
    table.cell(panel, 0, 1, "Swing", text_color = dim, text_size = txtSize, text_halign = text.align_left)
    table.cell(panel, 1, 1, swingTxt, text_color = showing ? colSw : txt, text_size = txtSize, text_halign = text.align_right)
    table.cell(panel, 0, 2, "Pullback", text_color = dim, text_size = txtSize, text_halign = text.align_left)
    table.cell(panel, 1, 2, pbTxt, text_color = pbClr, text_size = txtSize, text_halign = text.align_right)
    table.cell(panel, 0, 3, "Zone", text_color = dim, text_size = txtSize, text_halign = text.align_left)
    table.cell(panel, 1, 3, recTxt, text_color = txt, text_size = txtSize, text_halign = text.align_right)
    table.cell(panel, 0, 4, "Pullbacks here", text_color = dim, text_size = txtSize, text_halign = text.align_left)
    table.cell(panel, 1, 4, bandTxt, text_color = haveBand ? colExt : txt, text_size = txtSize, text_halign = text.align_right)
    table.cell(panel, 0, 5, "Targets", text_color = dim, text_size = txtSize, text_halign = text.align_left)
    table.cell(panel, 1, 5, tgtTxt, text_color = showing ? colExt : txt, text_size = txtSize, text_halign = text.align_right)
    table.cell(panel, 0, 6, "Read", text_color = dim, text_size = txtSize, text_halign = text.align_left)
    table.cell(panel, 1, 6, read, text_color = readClr, text_size = txtSize, text_halign = text.align_right)

// ───────────────────────── Alerts ─────────────────────────

alertcondition(enterZone, "In the zone",  "Fibonacci Master: price pulled into the 0.5 to 0.618 zone")
alertcondition(held,      "Zone held",    "Fibonacci Master: the pullback to the zone held")
alertcondition(failed,    "Zone failed",  "Fibonacci Master: the pullback to the zone failed")
alertcondition(hitNow,    "1.272 hit",    "Fibonacci Master: price reached the 1.272 target")
alertcondition(hit1618,   "1.618 hit",    "Fibonacci Master: price reached the 1.618 target")
alertcondition(flipped,   "New swing",    "Fibonacci Master: the swing flipped")